import * as lsp from "vscode-languageserver/node";
import * as fs from "fs";
import { isSupportedFile, isSupportedTriggerFile, sleep } from "../common/util";
import { Project } from "./project";
import {
    dsConfigurationToMDMSerializerOptions,
    dsWindowPropertiesUpdateParamsToScriptOptionUpdateParam,
    pathToUri,
    uriToPath
} from "./translation";
import { emptyCompletionList } from "./completion";
import { emptyHover } from "./hover";
import { isGeneratedFile, preprareMetadataFilePath } from "./util";
import { existsSync } from "fs";
import {
    DsWindowProperties,
    DsWindowPropertiesUpdateParams,
    SelectFileBranchParams,
    SetProgramMetadataParams,
} from "../common/types";
import { MDMDocument } from "./metadata";
import { LanguageServiceClient } from "./languageServiceClient";
import { createProjectWorkspaceManager, ProjectWorkspace } from "./workspace";
import { languageServerNotifications, languageServerRequests } from "./types";
import { getGlobalDefinitionPath, getGlobalDefinitionProgram, getTypeChecker, t } from "./analysis";
import { reportDiagnostics } from "./validate";
import { Task, TaskQueue } from "./taskQueue";

export interface LanguageServer extends lsp.Disposable {
    readonly capabilities: lsp.ServerCapabilities;
    initialize(params: lsp.InitializeParams): Promise<lsp.InitializeResult<any>>;
    initialized(_: lsp.InitializedParams): Promise<void>;
    initalizeConfigurations(configuration: DsWindowProperties): Promise<void>;
    didOpenTextDocument(param: lsp.DidOpenTextDocumentParams): Promise<void>;
    didChangeTextDocumentContent(param: lsp.DidChangeTextDocumentParams): Promise<void>;
    didCloseTextDocument(param: lsp.DidCloseTextDocumentParams): Promise<void>;
    provideCompletion(params: lsp.CompletionParams): Promise<lsp.CompletionList>;
    provideHover(params: lsp.HoverParams): Promise<lsp.Hover | null>;
    provideDefinition(params: lsp.TextDocumentPositionParams): Promise<lsp.Definition | lsp.DefinitionLink[] | null>;
    provideReferences(params: lsp.ReferenceParams): Promise<lsp.Location[] | null>;
    provideSignatureHelp(params: lsp.SignatureHelpParams): Promise<lsp.SignatureHelp | null>;
    provideCodeAction(params: lsp.CodeActionParams): Promise<lsp.CodeAction[]>;
    provideRename(params: lsp.RenameParams): Promise<lsp.WorkspaceEdit | null>;
    prepareRename(params: lsp.PrepareRenameParams): Promise<lsp.Range | undefined | null>;
    provideDocumentHighlight(params: lsp.DocumentHighlightParams): Promise<lsp.DocumentHighlight[] | undefined>;
    provideDocumentSymbol(params: lsp.DocumentSymbolParams): Promise<lsp.DocumentSymbol[] | undefined>;
    provideFoldingRanges(params: lsp.FoldingRangeParams): Promise<lsp.FoldingRange[] | null | undefined>;
    provideSemanticTokens(params: lsp.SemanticTokensParams): Promise<lsp.SemanticTokens>;
    provideSemanticTokensRange(params: lsp.SemanticTokensRangeParams): Promise<lsp.SemanticTokens>;
    didChangeWatchedFiles(params: lsp.DidChangeWatchedFilesParams): Promise<void>;
    updateConfiguration(param: DsWindowPropertiesUpdateParams): void;
    getFileBranches(param: string): Promise<string[]>;
    getFileCurrentBranch(param: string): Promise<string>;
    selectFileBranch(param: SelectFileBranchParams): Promise<void>;
    getProgramMetadata(uri: string): Promise<string | undefined>;
    setProgramMetadata(param: SetProgramMetadataParams): Promise<void>;
}

const globalDeclarationProject = new Project({
    rootPath: getGlobalDefinitionPath(),
    program: getGlobalDefinitionProgram(),
    locked: true,
});

export function createLanguageServer(client: LanguageServiceClient): LanguageServer {
    const workspaceManager = createProjectWorkspaceManager(initializeProject);
    workspaceManager.createWorkspace(globalDeclarationProject);
    workspaceManager.onLogMessage(logInfo);
    let configurationInitialized = false;
    let hasDisposed = false;

    const taskQueue = new TaskQueue();
    const pendingTask = new Set<number>();

    const capabilities: lsp.ServerCapabilities = {
        textDocumentSync: lsp.TextDocumentSyncKind.Incremental,
        workspace: {
            workspaceFolders: {
                supported: true,
                changeNotifications: true
            },
            fileOperations: {
                willRename: {
                    filters: [
                        {
                            pattern: {
                                glob: "**/*.{mrs,dms,ini,inc}"
                            }
                        }
                    ]
                }
            },
        },
        signatureHelpProvider: {
            triggerCharacters: [ "(", "," ],
            retriggerCharacters: [ ")" ]
        },
        completionProvider: {
            resolveProvider: false,
            triggerCharacters: [ ".", "\\", "/", "#", "\"", "{" ]
        },
        codeActionProvider: {
            resolveProvider: false,
            codeActionKinds: [ lsp.CodeActionKind.QuickFix ]
        },
        hoverProvider: true,
        renameProvider: true,
        definitionProvider: true,
        referencesProvider: true,
        documentHighlightProvider: true,
        documentSymbolProvider: true,
        foldingRangeProvider: true,
        semanticTokensProvider: {
            documentSelector: null,
            legend: {
                tokenTypes: [
                    'comment',
                    'string',
                    'keyword',
                    'number',
                    'regexp',
                    'operator',
                    'namespace',
                    'type',
                    'struct',
                    'class',
                    'interface',
                    'enum',
                    'enumMember',
                    'typeParameter',
                    'function',
                    'method',
                    'decorator',
                    'macro',
                    'variable',
                    'parameter',
                    'property',
                    'label'
                ],
                tokenModifiers: [
                    'declaration',
                    'documentation',
                    'readonly',
                    'static',
                    'abstract',
                    'deprecated',
                    'modification',
                    'async'
                ],
            },
            full: true,
            range: true,
        }
    };

    return {
        capabilities,
        initialize,
        initialized,
        initalizeConfigurations,
        didOpenTextDocument,
        didChangeTextDocumentContent,
        didCloseTextDocument,
        provideCompletion,
        provideHover,
        provideDefinition,
        provideReferences,
        provideSignatureHelp,
        provideCodeAction,
        provideRename,
        prepareRename,
        provideDocumentHighlight,
        provideDocumentSymbol,
        provideFoldingRanges,
        provideSemanticTokens,
        provideSemanticTokensRange,
        didChangeWatchedFiles,
        updateConfiguration,
        getFileBranches,
        getFileCurrentBranch,
        selectFileBranch,
        getProgramMetadata,
        setProgramMetadata,
        dispose,
    };

    function logInfo(message: string) {
        client.logMessage({ type: lsp.MessageType.Info, message });
    }

    async function initialize(_: lsp.InitializeParams): Promise<lsp.InitializeResult> {
        if (configurationInitialized) {
            logInfo("IBM DataCollection Script language server has initialized.");
            return { capabilities };
        }
        return { capabilities };
    }

    async function initialized(_: lsp.InitializedParams) {
    }

    async function initalizeConfigurations(configuration: DsWindowProperties): Promise<void> {
        workspaceManager.updateConfiguration(configuration);
        configurationInitialized = true;
    }

    function executeTask(task: Task): void {
    }

    function executeNextTask(): void {
        while(pendingTask.size === 0 && taskQueue.length > 0) {
            const task = taskQueue.dequeue();
            if (task) {
                task.callback();
            }
        }
    }

    async function promptSelectMetadataFile(rootPath: string) {
        return client.showInformationMessage(
            "是否指定元数据文件以提高语法提示的准确性?",
            { title: "快速选择" }, { title: "打开" }, { title: "关闭" }
        ).then(action => {
            if (action?.title === "快速选择") {
                return client.sendRequest(
                    languageServerRequests["dsclient/showQuickPickFile"],
                    {
                        folder: rootPath,
                        extens: [ ".mdd" ]
                    }
                );
            }
            if (action?.title === "打开") {
                return client.sendRequest(
                    languageServerRequests["dsclient/showOpenFileDialog"],
                    {
                        defaultPath: rootPath,
                        title: "选择元数文档",
                        canSelectMany: false,
                        filters: { "metadata file": [ "mdd" ] }
                    }
                ).then(selection => {
                    if (selection && selection.length > 0) {
                        return selection[0];
                    }
                    return undefined;
                });
            }
        });
    }

    function refreshTextDocumentsDiagnostics(workspace: ProjectWorkspace, updateFileFsPath?: string) {
        if (updateFileFsPath) {
            let relatives = workspace.project.program.getRelatives(updateFileFsPath, file => file.tracker.getSourceFile());
            publishDiagnostics({
                fsPath: updateFileFsPath,
                sourceFiles: relatives
            });
        }
        workspace.forEach(document => {
            let fsPath = uriToPath(document.uri);
            let sourceFile = workspace.project.program.getSourceFile(fsPath);
            if (sourceFile) {
                publishDiagnostics({
                    fsPath,
                    sourceFile
                });
            }
        });
    }

    async function withProgress<R>(message: string, task: () => Promise<R>): Promise<R> {
        return client.createProgressReporter().then(reporter => client.withProgress({ reporter, message }, task));
    }

    async function setLocalMetadata(workspace: ProjectWorkspace, file: string, metadataChanged = false) {
        withProgress(
            `初始化元数据 (${file})`,
            async () => {
                workspace.project.program.loadLocalMetadataFile(file);
                if (metadataChanged) {
                    client.sendNotification(
                        languageServerNotifications["dsclient/workspaceConfigurationChanged"],
                        {
                            rootPath: workspace.project.rootPath,
                            properties: {
                                metadata: file
                            }
                        }
                    );
                }
                refreshTextDocumentsDiagnostics(workspace);
            }
        );
    }

    async function didOpenTextDocument(param: lsp.DidOpenTextDocumentParams) {
        let fsPath = uriToPath(param.textDocument.uri);
        if (!isSupportedTriggerFile(fsPath)) {
            return;
        }
        // 如果文件为 .mdd 扩展名，创建临时文本文件，并更改打开的文档
        if (fsPath.toLowerCase().endsWith(".mdd")) {
            let preparedPath = preprareMetadataFilePath(fsPath);
            withProgress(
                `读取元数据文档 (${fsPath})`,
                async () => {
                    let document = new MDMDocument();
                    try {
                        document.open(fsPath);
                        const properties = await client.sendRequest(languageServerRequests["dsclient/getConfiguration"], fsPath);
                        const config = dsConfigurationToMDMSerializerOptions(properties);
                        document.exportSerializedFile(preparedPath, config);
                        if (properties.closeMddAfterSerialization) {
                            client.sendNotification(languageServerNotifications["dsclient/closeActivateEditor"], fsPath);
                        }
                        client.sendNotification(languageServerNotifications["dsclient/showEditor"], preparedPath.scriptFile);
                    }
                    catch (error) {
                        if (error instanceof Error) {
                            client.showErrorMessage(`读取元数据文件失败: ${error.message}`);
                        }
                        else {
                            throw error;
                        }
                    }
                    return document;
                }
            );
            return;
        }
        // .mdd文件不计入工作空间
        let workspace = workspaceManager.getWorkspace(fsPath);
        if (!workspace) {
            let rootPath = workspaceManager.getWorkspaceRoot(fsPath);
            let lowerRoot = rootPath.toLowerCase();
            if (workspaceManager.isLoading(lowerRoot)) {
                workspace = await waitForLoadingWorkspace(lowerRoot);
            }
            if (!workspace) {
                workspace = await workspaceManager.createWorkspace(
                    rootPath,
                    [{ fsPath, item: param.textDocument }]
                );
            }
        }
        workspace.openTextDocument(fsPath, param.textDocument);
        publishDiagnostics({ fsPath, workspace });
    }

    async function didChangeTextDocumentContent(param: lsp.DidChangeTextDocumentParams) {
        let fsPath = uriToPath(param.textDocument.uri);
        if (!isSupportedTriggerFile(fsPath)) {
            return;
        }
        let workspace = workspaceManager.getWorkspace(fsPath);
        if (workspace) {
            workspace.updateTextDocument(fsPath, param.contentChanges);
            publishDiagnostics({
                fsPath,
                workspace,
            });
        }
    }

    async function didCloseTextDocument(param: lsp.DidCloseTextDocumentParams) {
        let fsPath = uriToPath(param.textDocument.uri);
        if (!isSupportedTriggerFile(fsPath)) {
            return;
        }
        client.publishDiagnostics({ uri: param.textDocument.uri, diagnostics: [] });
        if (fsPath.toLowerCase().endsWith(".mdd")) {
            return;
        }
        let workspace = workspaceManager.getWorkspace(fsPath);
        if (workspace) {
            workspace.closeTextDocument(fsPath);
        }
        clearDiagnostics(fsPath);
    }

    async function onFileCreated(fsPath: string) {
        if (isGeneratedFile(fsPath) || !isSupportedFile(fsPath)) {
            return;
        }
        const workspace = await getWorkspace(fsPath);
        if (workspace) {
            workspace.createTextDocument(fsPath);
            refreshTextDocumentsDiagnostics(workspace);
        }
    }

    async function onFileDeleted(fsPath: string) {
        if (isGeneratedFile(fsPath) || !isSupportedFile(fsPath)) {
            return;
        }
        const workspace = await getWorkspace(fsPath);
        if (workspace) {
            workspace.deleteTextDocument(fsPath);
            refreshTextDocumentsDiagnostics(workspace);
        }
    }

    async function didChangeWatchedFiles(params: lsp.DidChangeWatchedFilesParams) {
        if (params.changes.length > 0) {
            for (const change of params.changes) {
                if (change.type === lsp.FileChangeType.Created) {
                    onFileCreated(uriToPath(change.uri));
                }
                else if (change.type === lsp.FileChangeType.Deleted) {
                    onFileDeleted(uriToPath(change.uri));
                }
            }
        }
    }

    async function initializeProject(fsPath: string): Promise<Project> {
        return await withProgress(
            `载入项目 (${fsPath})`,
            async () => {
                const project = new Project({
                    rootPath: fsPath,
                });
                return project;
            }
        );
    }

    async function waitForLoadingWorkspace(fsPath: string) {
        while (workspaceManager.isLoading(fsPath)) {
            await sleep(200);
        }
        return workspaceManager.getWorkspace(fsPath);
    }

    async function getWorkspace(fsPath: string): Promise<ProjectWorkspace | undefined> {
        if (!isSupportedFile(fsPath)) {
            return undefined;
        }
        let rootPath = workspaceManager.getWorkspaceRoot(fsPath);
        let lowerPath = rootPath.toLowerCase();
        // 全局声明文件
        if (lowerPath.startsWith(getGlobalDefinitionPath().toLowerCase())) {
            return workspaceManager.getWorkspace(getGlobalDefinitionPath());
        }
        let workspace = workspaceManager.getWorkspace(lowerPath);
        if (workspace && !workspaceManager.isLoading(lowerPath)) {
            return workspace;
        }
        if (!workspace && workspaceManager.isLoading(lowerPath)) {
            return await waitForLoadingWorkspace(lowerPath);
        }
        if (!workspace) {
            workspace = await workspaceManager.createWorkspace(rootPath, 1);
        }
        // 对于MDMDocument输出的元数据文件和路由代码文件，单独创建Project对象，不计入所在文件夹工作空间。
        let config = await client.sendRequest(languageServerRequests["dsclient/getConfiguration"], workspace.rootPath);
        if (config.metadata && existsSync(config.metadata)) {
            let metadata = config.metadata;
            withProgress(
                `初始化元数据 (${metadata})`,
                async () => workspace?.project.program.loadLocalMetadataFile(metadata)
            ).then(() => {
                refreshTextDocumentsDiagnostics(workspace!);
            });
        }
        else if (!isGeneratedFile(workspace.rootPath)) {
            let config = await client.sendRequest(languageServerRequests["dsclient/getWindowConfiguration"], undefined);
            if (config.metadata && config.metadata.toLowerCase().endsWith(".mdd") && fs.existsSync(config.metadata)) {
                setLocalMetadata(workspace, config.metadata);
            }
            else if (config.promptMetadataWhenOpenFile) {
                promptSelectMetadataFile(workspace.rootPath).then(metadataFile => {
                    if (metadataFile) {
                        setLocalMetadata(workspace!, metadataFile, true);
                    }
                });
            }
        }
        return workspace;
    }

    async function clearDiagnostics(fsPath: string) {
        client.publishDiagnostics({ uri: pathToUri(fsPath), diagnostics: [] });
    }

    interface PublishDiagnosticParam {
        fsPath: string;
        workspace?: ProjectWorkspace;
        sourceFiles?: t.SourceFile[];
        sourceFile?: t.SourceFile;
    }

    async function publishDiagnostics(param: PublishDiagnosticParam) {
        if (hasDisposed) {
            return;
        }
        try {
            let lowerPath = param.fsPath.toLowerCase();
            if (lowerPath.endsWith(".mdd")) {
                return;
            }
            else {
                let workspace: ProjectWorkspace | undefined = param.workspace;
                if (!workspace) {
                    workspace = await getWorkspace(param.fsPath);
                }
                if (workspace) {
                    if (param.sourceFiles) {
                        for (const source of param.sourceFiles) {
                            let document = workspace.getTextDocument(source.fileName);
                            if (document) {
                                client.publishDiagnostics({
                                    uri: pathToUri(source.fileName),
                                    diagnostics: await reportDiagnostics(source.diagnostics, document)
                                });
                            }
                            else {
                                clearDiagnostics(source.fileName);
                            }
                        }
                    }
                    else if (param.sourceFile) {
                        let document = workspace.getTextDocument(param.sourceFile.fileName);
                        if (document) {
                            client.publishDiagnostics({
                                uri: pathToUri(param.sourceFile.fileName),
                                diagnostics: await reportDiagnostics(param.sourceFile.diagnostics, document)
                            });
                        }
                        else {
                            clearDiagnostics(param.sourceFile.fileName);
                        }
                    }
                    else {
                        let validateResult = await workspace.project.provideDiagnostics({
                            fsPath: param.fsPath,
                            workspace,
                            requestReferenceFiles: true,
                        });
                        if (validateResult) {
                            for (const item of validateResult) {
                                client.publishDiagnostics(item);
                            }
                        }
                    }
                }
                else {
                    clearDiagnostics(param.fsPath);
                }
            }
        }
        catch (error) {
            if (error instanceof Error) {
                logInfo(`错误: ${error.message}`);
            }
            else {
                throw error;
            }
        }
    }

    async function provideCompletion(params: lsp.CompletionParams): Promise<lsp.CompletionList> {
        return getWorkspace(uriToPath(params.textDocument.uri)).then(workspace => workspace?.project.provideCompletion(params, workspace) ?? emptyCompletionList);
    }

    async function provideHover(params: lsp.HoverParams): Promise<lsp.Hover | null> {
        return getWorkspace(uriToPath(params.textDocument.uri)).then(workspace => workspace?.project.provideHover(params, workspace) ?? emptyHover);
    }

    async function provideDefinition(params: lsp.TextDocumentPositionParams): Promise<lsp.Definition | lsp.DefinitionLink[] | null> {
        return getWorkspace(uriToPath(params.textDocument.uri)).then(workspace => workspace?.project.provideDefinition(params, workspace) ?? null);
    }

    async function provideReferences(params: lsp.ReferenceParams): Promise<lsp.Location[] | null> {
        return getWorkspace(uriToPath(params.textDocument.uri)).then(workspace => workspace?.project.provideReferences(params, workspace) ?? null);
    }

    async function provideSignatureHelp(params: lsp.SignatureHelpParams): Promise<lsp.SignatureHelp | null> {
        return getWorkspace(uriToPath(params.textDocument.uri)).then(workspace => workspace?.project.provideSignatureHelp(params, workspace) ?? null);
    }

    async function provideCodeAction(params: lsp.CodeActionParams): Promise<lsp.CodeAction[]> {
        return getWorkspace(uriToPath(params.textDocument.uri)).then(workspace => workspace?.project.provideCodeAction(params) ?? []);
    }

    async function provideRename(params: lsp.RenameParams): Promise<lsp.WorkspaceEdit | null> {
        return getWorkspace(uriToPath(params.textDocument.uri)).then(workspace => workspace?.project.provideRenameRequest(params, workspace) ?? null);
    }

    async function prepareRename(params: lsp.PrepareRenameParams): Promise<lsp.Range | undefined | null> {
        return getWorkspace(uriToPath(params.textDocument.uri)).then(workspace => workspace?.project.providePrepareRenameRequest(params, workspace) ?? null);
    }

    async function provideDocumentHighlight(params: lsp.DocumentHighlightParams): Promise<lsp.DocumentHighlight[] | undefined> {
        return getWorkspace(uriToPath(params.textDocument.uri)).then(workspace => workspace?.project.provideDocumentHighlight(params, workspace));
    }

    async function provideDocumentSymbol(params: lsp.DocumentSymbolParams): Promise<lsp.DocumentSymbol[] | undefined> {
        return getWorkspace(uriToPath(params.textDocument.uri)).then(workspace => workspace?.project.provideDocumentSymbol(params));
    }

    async function provideFoldingRanges(params: lsp.FoldingRangeParams): Promise<lsp.FoldingRange[] | null | undefined> {
        return getWorkspace(uriToPath(params.textDocument.uri)).then(workspace => workspace?.project.provideFoldingRanges(params));
    }

    async function provideSemanticTokens(params: lsp.SemanticTokensParams): Promise<lsp.SemanticTokens> {
        return getWorkspace(uriToPath(params.textDocument.uri)).then(workspace => workspace?.project.provideSemanticTokens(params) ?? { data: [] });
    }

    async function provideSemanticTokensRange(params: lsp.SemanticTokensRangeParams): Promise<lsp.SemanticTokens> {
        return getWorkspace(uriToPath(params.textDocument.uri)).then(workspace => workspace?.project.provideSemanticTokensRange(params, workspace) ?? { data: [] });
    }

    function updateConfiguration(param: DsWindowPropertiesUpdateParams): void {
        const checker = getTypeChecker();
        checker.updateOptions(dsWindowPropertiesUpdateParamsToScriptOptionUpdateParam(param));
    }

    async function getFileBranches(fsPath: string): Promise<string[]> {
        const workspace = await getWorkspace(fsPath);
        const file = workspace?.project.program.getFile(fsPath);
        if (file) {
            return file.tracker.branches.map(e => e.entry.path);
        }
        return [];
    }

    async function getFileCurrentBranch(fsPath: string): Promise<string> {
        const workspace = await getWorkspace(fsPath);
        const file = workspace?.project.program.getFile(fsPath);
        if (file) {
            return file.tracker.getBranch().entry.path;
        }
        return "";
    }

    async function selectFileBranch(param: SelectFileBranchParams): Promise<void> {
        const fsPath = param.fsPath;
        const workspace = await getWorkspace(fsPath);
        const file = workspace?.project.program.getFile(fsPath);
        if (workspace && file) {
            const branch = file.tracker.lockBranch(param.selectBranch);
            if (branch) {
                let source = branch.source;
                if (source) {
                    publishDiagnostics({
                        fsPath,
                        sourceFile: source
                    });
                }
            }
        }
    }

    async function getProgramMetadata(fsPath: string): Promise<string | undefined> {
        const workspace = await getWorkspace(fsPath);
        return workspace?.project.program.localMetadata;
    }

    async function setProgramMetadata(param: SetProgramMetadataParams): Promise<void> {
        const fsPath = uriToPath(param.uri);
        const workspace = await getWorkspace(fsPath);
        if (workspace) {
            withProgress(
                `初始化元数据 (${uriToPath(fsPath)})`,
                async () => workspace.project.program.loadLocalMetadataFile(param.metadata)
            );
        }
    }

    function dispose() {
        workspaceManager.dispose();
        hasDisposed = true;
    }

}

